package ssh

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	// "gitee.com/dark.H/darknet/progress"
	"gitee.com/dark.H/gs"
	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/terminal"
	"golang.org/x/net/proxy"
)

type SSH struct {
	gs.Str
	Client      *ssh.Client
	Session     *ssh.Session
	clientConf  *ssh.ClientConfig
	Proxy       gs.Str
	stdout      io.Reader
	stdin       io.Writer
	stderr      io.Reader
	CacheBuffer *bytes.Buffer
	Host        gs.Str
	historys    []string
	Err         error
	Onkey       func(line string)
	call        func(sshc *SSH, line gs.Str)
}

func GetIP(buf gs.Str) (ips gs.Strs, found bool) {
	ips = buf.Find(`\d{1,3}\.\d{1.3}\.\d{1,3}\.\d{1,3}`)
	found = ips.Len() > 0
	return
}

func GetSSH(buf gs.Str) (sshs gs.Strs, found bool) {
	sshs = buf.Find(`\w*\@[\.\w\-\_\:]`)
	found = sshs.Len() > 0
	return
}

func proxiedSSHClient(proxyAddress, sshServerAddress string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
	dialer, err := proxy.SOCKS5("tcp", proxyAddress, nil, proxy.Direct)
	if err != nil {
		return nil, err
	}

	conn, err := dialer.Dial("tcp", sshServerAddress)
	if err != nil {
		return nil, err
	}

	c, chans, reqs, err := ssh.NewClientConn(conn, sshServerAddress, sshConfig)
	if err != nil {
		return nil, err
	}

	return ssh.NewClient(c, chans, reqs), nil
}

func AsSSH(target gs.Str, pwds ...gs.Str) *SSH {
	host := target
	if !host.In(":") {
		host = host.Add(":22")
	}
	user := gs.Str("root")
	if target.In("@") {
		fs := target.Split("@", 2)
		user = fs[0]
		host = fs[1]
	}
	pwd := ""

	keyPath := gs.HOME.PathJoin(".ssh", "id_rsa")
	proxy := gs.Str("")
	if pwds != nil {
		pwd = pwds[0].Str()
		if len(pwds) > 1 {
			proxy = pwds[1]
			if proxy.StartsWith("socks5://") {
				proxy = proxy.Split("socks5://").Last()
			}
		}
	}

	if gs.Str(pwd).IsExists() {
		keyPath = gs.Str(pwd)
	}

	key, err := ioutil.ReadFile(keyPath.Str())
	if err != nil {
		gs.Str(err.Error()).ANSIRed().Println()
		return nil
	}
	signer, err := ssh.ParsePrivateKey(key)
	if err != nil {
		gs.Str(err.Error()).ANSIRed().Println()
		return nil
	}
	var clientConf *ssh.ClientConfig
	if pwds != nil {
		clientConf = &ssh.ClientConfig{
			User: user.String(),
			Auth: []ssh.AuthMethod{
				ssh.Password(pwd),
			},
			Timeout:         30 * time.Second,
			HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		}
	} else {
		clientConf = &ssh.ClientConfig{
			User: user.Str(),
			Auth: []ssh.AuthMethod{
				ssh.PublicKeys(signer),
			},
			Timeout:         30 * time.Second,
			HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		}
	}

	if proxy == "" {
		client, err := ssh.Dial("tcp", host.Str(), clientConf)
		if err != nil {
			gs.Str(err.Error()).ANSIRed().Println()
			return nil
		}
		return &SSH{
			Str:        target,
			Client:     client,
			Err:        err,
			clientConf: clientConf,
			Host:       host,
			Onkey: func(line string) {

			},
			CacheBuffer: bytes.NewBuffer([]byte{}),
		}

	} else {
		client, err := proxiedSSHClient(string(proxy), host.Str(), clientConf)
		if err != nil {
			gs.Str(err.Error()).ANSIRed().Println()
			return nil
		}
		return &SSH{
			Str:        target,
			Client:     client,
			Err:        err,
			clientConf: clientConf,
			Proxy:      proxy,
			Host:       host,
			Onkey: func(line string) {

			},
			CacheBuffer: bytes.NewBuffer([]byte{}),
		}

	}
}

func (sshs *SSH) NewClient() *ssh.Client {
	if sshs.Proxy == "" {
		client, err := ssh.Dial("tcp", sshs.Host.Str(), sshs.clientConf)
		if err != nil {
			gs.Str(err.Error()).ANSIRed().Println()
			return nil
		}
		return client

	} else {
		client, err := proxiedSSHClient(sshs.Proxy.Str(), sshs.Host.Str(), sshs.clientConf)
		if err != nil {
			gs.Str(err.Error()).ANSIRed().Println()
			return nil
		}
		return client
	}
}

// func (sshs *SSH) Get(filename string) {
// 	client := sshs.NewClient()

// 	sftpclient, err := sftp.NewClient(client)
// 	if err != nil {
// 		log.Println(err)
// 	}
// 	defer sftpclient.Close()

// 	f := gs.Str(filename)
// 	ste, err := sftpclient.Stat(filename)
// 	if err != nil {
// 		return
// 	}
// 	if !ste.IsDir() {

// 		_, fw, err := f.Basename().OpenFile(gs.O_NEW_WRITE)
// 		if err != nil {
// 			return
// 		}
// 		defer fw.Close()
// 		fr, err := sftpclient.Open(filename)
// 		ste, _ := fr.Stat()
// 		allsize := ste.Size()
// 		ctx := context.Background()
// 		progressR := progress.NewReader(fr)
// 		go func() {
// 			// ctx := context.Background()
// 			_, width := gs.GetWindowsSize()
// 			width -= 10
// 			cs := progress.NewTicker(ctx, progressR, allsize, 3*time.Second)
// 			for p := range cs {
// 				ss := gs.Str("Get: %s | %% %.2f ").F(f.Basename(), p.Percent()).ANSIGreen()
// 				left := width - len(ss)
// 				ss += gs.Str(strings.Repeat(" ", left))
// 				l := width * int(p.Percent()) / 100
// 				sspre := ss[:l].ANSISelected()
// 				ssend := ss[l:]
// 				gs.Str("").ANSISave().ANSIHideOrDisCursor().ANSICursor(1, 1).ANSIClearThisLine().Add(sspre + ssend).ANSIRestore().Print()
// 			}

// 			gs.Str("").ANSISave().ANSIHideOrDisCursor().ANSICursor(1, 1).ANSIClearThisLine().Add("Download completed!" + f.Basename()).ANSIHideOrDisCursor().ANSIRestore().Print()
// 		}()
// 		if err != nil {
// 			return
// 		}
// 		io.Copy(fw, progressR)
// 		defer fr.Close()

// 	}
// }

// func (sshs *SSH) Put(filename string) {
// 	client := sshs.NewClient()

// 	session, err := client.NewSession()
// 	if err != nil {
// 		log.Println(err)
// 	}
// 	sftpclient, err := sftp.NewClient(client)
// 	if err != nil {
// 		log.Println(err)
// 	}
// 	defer sftpclient.Close()
// 	pwd, err := session.Output("pwd")
// 	if err != nil {
// 		log.Println(err)
// 		return
// 	}
// 	f := gs.Str(filename)
// 	if f.Exists() != "" {
// 		dstPath := gs.Str(pwd).Trim() + "/" + f.Basename()
// 		fw, err := sftpclient.Create(dstPath.Str())
// 		if err != nil {
// 			return
// 		}
// 		defer fw.Close()
// 		fr, err := os.Open(f.Str())
// 		ste, _ := fr.Stat()
// 		allsize := ste.Size()
// 		ctx := context.Background()
// 		progressR := progress.NewReader(fr)
// 		go func() {
// 			// ctx := context.Background()
// 			_, width := gs.GetWindowsSize()
// 			width -= 10
// 			cs := progress.NewTicker(ctx, progressR, allsize, 3*time.Second)
// 			for p := range cs {
// 				ss := gs.Str("Upload: %s | %% %.2f ").F(f.Basename(), p.Percent()).ANSIGreen()
// 				left := width - len(ss)
// 				ss += gs.Str(strings.Repeat(" ", left))
// 				l := width * int(p.Percent()) / 100
// 				sspre := ss[:l].ANSISelected()
// 				ssend := ss[l:]
// 				gs.Str("").ANSISave().ANSIHideOrDisCursor().ANSICursor(1, 1).ANSIClearThisLine().Add(sspre + ssend).ANSIRestore().Print()
// 			}

// 			gs.Str("").ANSISave().ANSIHideOrDisCursor().ANSICursor(1, 1).ANSIClearThisLine().Add("Upload completed!" + f.Basename()).ANSIHideOrDisCursor().ANSIRestore().Print()
// 		}()
// 		if err != nil {
// 			return
// 		}
// 		io.Copy(fw, progressR)
// 		defer fr.Close()

// 	}
// }

func (sshs *SSH) SetOnInput(call func(sshc *SSH, line gs.Str)) {
	sshs.call = call
}

func (sshs *SSH) OnInput(buf []byte) {
	// gs.Str(buf).Replace("\r", "\n").ToFile("/tmp/test2.log")
	sshs.CacheBuffer.Write(buf)
	if bytes.Contains(buf, []byte("\r")) {
		// ps := bytes.SplitN(buf, []byte("\n"), 2)
		lines, err := sshs.CacheBuffer.ReadString(byte('\r'))

		if err != nil {
			return
		}
		line := gs.Str(lines).Trim()
		if line != "" && sshs.call != nil {
			sshs.call(sshs, line)
		}
		// if line.Str() == "@put" {
		// 	fs := gs.List[gs.Str]{}
		// 	gs.Str(".").Ls().Every(func(ix int, i gs.Str) {
		// 		if !i.IsDir() {
		// 			fs = fs.Add(i)
		// 		}
		// 	})
		// 	f := console.Select(fs)
		// 	go sshs.Put(f.Str())
		// } else if line.StartsWith("@get ") {
		// 	f := line.Split("@get ").Last().Trim()
		// 	go sshs.Get(f.Str())
		// } else {
		// 	sshs.Onkey(strings.TrimSpace(lines))
		// }

	}
	// // tmp := io.TeeReader(os.Stdin, sshs.tmpCacheBuffer)
	// // e := time.NewTimer(100 * time.Millisecond)
	// // for {
	// // 	select {
	// // 	case <-e.C:
	// // 		ioutil.N
	// // 	default:
	// // 		time.Sleep(100 * time.Millisecond)
	// // 	}
	// // }

	// p := gs.Str(os.TempDir()).PathJoin("pip-stdio.unix")
	// stdPipefile, err := os.OpenFile(p.Str(), os.O_RDWR|os.O_TRUNC|os.O_CREATE, os.ModePerm)
	// if err != nil {
	// 	panic(err)
	// }
	// go io.Copy(stdPipefile, os.Stdin)
	// return stdPipefile, int(stdPipefile.Fd())
}

func (sshs *SSH) Terminal() (err error) {
	// 建立新会话
	session, err := sshs.Client.NewSession()
	sshs.Session = session
	defer session.Close()
	if err != nil {
		log.Fatalf("new session error: %s", err.Error())
	}

	// session.Stdout = os.Stdout // 会话输出关联到系统标准输出设备
	// session.Stderr = os.Stderr // 会话错误输出关联到系统标准错误输出设备
	// session.Stdin = os.Stdin   // 会话输入关联到系统标准输入设备
	// modes := ssh.TerminalModes{
	// 	ssh.ECHO:          0,     // 禁用回显（0禁用，1启动）
	// 	ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
	// 	ssh.TTY_OP_OSPEED: 14400, //output speed = 14.4kbaud
	// }

	fd := int(os.Stdin.Fd())
	// stdpipe, fd := sshs.OnInput()
	// defer stdpipe.Close()

	state, err := terminal.MakeRaw(fd)
	if err != nil {
		panic(err)
		return err
	}
	defer terminal.Restore(fd, state)

	w, h, err := terminal.GetSize(fd)
	if err != nil {
		return fmt.Errorf("terminal get size: %s", err)
	}

	modes := ssh.TerminalModes{
		ssh.ECHO:          1,
		ssh.TTY_OP_ISPEED: 14400,
		ssh.TTY_OP_OSPEED: 14400,
	}

	term := os.Getenv("TERM")
	if term == "" {
		term = "xterm-256color"
	}
	// h -= 1
	// w -= 1
	if err := session.RequestPty(term, h, w, modes); err != nil {
		return fmt.Errorf("session xterm: %s", err)
	}

	sshs.updateTerminalSize()

	sshs.stdin, err = sshs.Session.StdinPipe()
	if err != nil {
		return err
	}
	sshs.stdout, err = sshs.Session.StdoutPipe()
	if err != nil {
		return err
	}
	sshs.stderr, err = sshs.Session.StderrPipe()

	go io.Copy(os.Stderr, sshs.stderr)
	go io.Copy(os.Stdout, sshs.stdout)
	go func() {
		buf := make([]byte, 128)
		for {
			n, err := os.Stdin.Read(buf)
			if err != nil {
				fmt.Println(err)
				return
			}
			if n > 0 {
				sshs.OnInput(buf[:n])
				_, err = sshs.stdin.Write(buf[:n])
				if err != nil {
					fmt.Println(err)
					// sshs.exitMsg = err.Error()
					return
				}
			}
		}
	}()

	if err = session.Shell(); err != nil {
		log.Fatalf("start shell error: %s", err.Error())
	}
	if err = session.Wait(); err != nil {
		log.Fatalf("return error: %s", err.Error())
	}
	return nil
}

func (t *SSH) updateTerminalSize() {

	go func() {
		// SIGWINCH is sent to the process when the window size of the terminal has
		// changed.
		sigwinchCh := make(chan os.Signal, 1)
		signal.Notify(sigwinchCh, syscall.SIGWINCH)

		fd := int(os.Stdin.Fd())
		termWidth, termHeight, err := terminal.GetSize(fd)
		if err != nil {
			fmt.Println(err)
		}

		for {
			select {
			// The client updated the size of the local PTY. This change needs to occur
			// on the server side PTY as well.
			case sigwinch := <-sigwinchCh:
				if sigwinch == nil {
					return
				}
				currTermWidth, currTermHeight, err := terminal.GetSize(fd)

				// Terminal size has not changed, don't do anything.
				if currTermHeight == termHeight && currTermWidth == termWidth {
					continue
				}

				t.Session.WindowChange(currTermHeight, currTermWidth)
				if err != nil {
					fmt.Printf("Unable to send window-change reqest: %s.", err)
					continue
				}

				termWidth, termHeight = currTermWidth, currTermHeight

			}
		}
	}()

}
